# Copyright (C) 2011 Nippon Telegraph and Telephone Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

####
#### josh@felllight.com - slightly less tragic simple switch
####
#### main changes
####
####  * delete all flows on initialization
####  * use barriers to enforce ordering
####  * controller does not ever packet out - it uses dataplane to flood (probably only works on OVS and OVS type switches)
####  * controller needs only partial packets to do MAC learning
####  * handles host moves
####
#### todo
####
####  * implement LLDP
####  * use goto_table for better flow table size scaling (not working on PicOS 2.1)
####

import pickle
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib import  hub


class SimpleSwitch13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    ETHERTYPES = (0x0800, 0x0806, 0x86DD)
    

    def __init__(self, *args, **kwargs):
        super(SimpleSwitch13, self).__init__(*args, **kwargs)
        self.mac_to_port = {}
        self.stats_event = hub.Event()
        self.threads.append(hub.spawn(self.stats_loop))
        self.datapaths = [];


    def stats_loop(self):
       while self.is_active:
            self.stats_event.clear()
            self.logger.debug('Tic Toc, Tic Toc')
	    for datapath in self.datapaths:
		self.logger.debug('Datapath = %s',datapath)
                self.send_port_stats_request(datapath)
            self.stats_event.wait(timeout=5)


    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        parser = datapath.ofproto_parser
        dpid = datapath.id

        self.mac_to_port[dpid] = {}

        self.delete_all_flows(datapath, parser)

        self.add_controller_flood_flow(datapath, parser)
        
	if datapath not in self.datapaths:
	   self.datapaths.append(datapath)

    def send_port_stats_request(self, datapath):
       ofp = datapath.ofproto
       ofp_parser = datapath.ofproto_parser

       req = ofp_parser.OFPPortStatsRequest(datapath, 0, ofp.OFPP_ANY)
       datapath.send_msg(req)

    @set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER)
    def port_stats_reply_handler(self, ev):
        ports = []
        for stat in ev.msg.body:
            ports.append('port_no=%d '
                         'rx_packets=%d tx_packets=%d '
                         'rx_bytes=%d tx_bytes=%d '
                         'rx_dropped=%d tx_dropped=%d '
                         'rx_errors=%d tx_errors=%d '
                         'rx_frame_err=%d rx_over_err=%d rx_crc_err=%d '
                         'collisions=%d duration_sec=%d duration_nsec=%d' %
                         (stat.port_no,
                          stat.rx_packets, stat.tx_packets,
                          stat.rx_bytes, stat.tx_bytes,
                          stat.rx_dropped, stat.tx_dropped,
                          stat.rx_errors, stat.tx_errors,
                          stat.rx_frame_err, stat.rx_over_err,
                          stat.rx_crc_err, stat.collisions,
                          stat.duration_sec, stat.duration_nsec))
        self.logger.debug('PortStats: %s', ports)
        dpid = ev.msg.datapath.id
	filename = ('/tmp/data_br%s.pkl' % dpid)	
        output = open(filename, 'wb')
        pickle.dump(ports, output)

    def send_mod(self, datapath, mod):
        datapath.send_msg(mod)

    def send_barrier_and_mod(self, datapath, parser, mod):
        barrier = parser.OFPBarrierRequest(datapath) 
        self.send_mod(datapath, mod)
        datapath.send_msg(barrier)

    def delete_flows(self, datapath, parser, match):
        ofproto = datapath.ofproto
        self.logger.debug('PortStats: %s', ports)

    def send_mod(self, datapath, mod):
        datapath.send_msg(mod)

    def send_barrier_and_mod(self, datapath, parser, mod):
        barrier = parser.OFPBarrierRequest(datapath) 
        self.send_mod(datapath, mod)
        datapath.send_msg(barrier)

    def delete_flows(self, datapath, parser, match):
        ofproto = datapath.ofproto
        mod = parser.OFPFlowMod(datapath, 0, 0,
                                ofproto.OFPTT_ALL,
                                ofproto.OFPFC_DELETE,
                                0, 0, 0, 0xffffffff,
                                ofproto.OFPP_ANY,
                                ofproto.OFPG_ANY,
                                0, match, [])
        self.send_barrier_and_mod(datapath, parser, mod)

    def delete_all_flows(self, datapath, parser):
        ofproto = datapath.ofproto 
        match = parser.OFPMatch()
        self.delete_flows(datapath, parser, match)

    def add_controller_flood_flow(self, datapath, parser):
        ofproto = datapath.ofproto
        match = parser.OFPMatch()
        # dataplane will default flood for us, so we don't need a
        # complete packet - just the ethernet headers
        actions = [parser.OFPActionOutput(ofproto.OFPP_FLOOD),
                   parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          64)]
                                          
        for eth_type in self.ETHERTYPES:
            match = parser.OFPMatch(eth_type=eth_type)
            self.add_flow(datapath, 0, 0, match, actions)

    def add_flow(self, datapath, priority, table_id, match, actions):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]

        mod = parser.OFPFlowMod(datapath=datapath,
                                table_id=table_id,
                                priority=priority,
                                match=match,
                                instructions=inst)
        self.send_barrier_and_mod(datapath, parser, mod)

    def add_goto(self, datapath, priority, table_id, match):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        inst = [parser.OFPInstructionGotoTable(1)]

        mod = parser.OFPFlowMod(datapath=datapath,
                                table_id=table_id,
                                priority=priority,
                                match=match,
                                instructions=inst)
        self.send_barrier_and_mod(datapath, parser, mod)

    def add_drop_flow(self, datapath, priority, table_id):
        ofproto = datapath.ofproto
        actions = []
        self.add_flow(datapath, priority, table_id, match, actions)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']

	if in_port >= ofproto.OFPP_MAX:
            return

        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]

        dst = eth.dst
        src = eth.src

        dpid = datapath.id

        self.mac_to_port.setdefault(dpid, {})
  
        self.logger.info('packet in from %s port %s to %s on dpid %s',
                         src, in_port, dst, dpid)


        if src in self.mac_to_port[dpid]:
            old_in_port = self.mac_to_port[dpid][src]
            if old_in_port != in_port:
                self.logger.info('host %s moved from port %s on dpid %s',
                                 src, old_in_port, dpid)
                del self.mac_to_port[dpid][src]
                for eth_type in self.ETHERTYPES:
                    match = parser.OFPMatch(in_port=old_in_port, eth_src=src, eth_type=eth_type)
                    self.delete_flows(datapath, parser, match)
                    match = parser.OFPMatch(eth_dst=src, eth_type=eth_type)
                    self.delete_flows(datapath, parser, match)

        if src not in self.mac_to_port[dpid]:
            self.logger.info('associate %s with port %s on dpid %s',
                             src, in_port, dpid)
            self.mac_to_port[dpid][src] = in_port
            # add broadcast and multicast flows from this host
            # prevents future broadcasts and multicasts from this host
            # from coming up to controller.
            # add broadcast flow
            for eth_type in self.ETHERTYPES:
                match = parser.OFPMatch(in_port=in_port,
                                        eth_src=src,
                                        eth_dst='ff:ff:ff:ff:ff:ff',
                                        eth_type=eth_type)
                actions = [parser.OFPActionOutput(ofproto.OFPP_ALL)]
                self.add_flow(datapath, 10, 0, match, actions)
                # add multicast flow
                match = parser.OFPMatch(in_port=in_port,
                                        eth_src=src,
                                        eth_dst=('01:00:00:00:00:00',
                                                 '01:00:00:00:00:00'),
                                        eth_type=eth_type)
                self.add_flow(datapath, 10, 0, match, actions)

        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
            self.logger.info('add unicast flow from %s port %s to %s port %s on dpid %s',
                             src, in_port, dst, out_port, dpid)
            for eth_type in self.ETHERTYPES: 
                match = parser.OFPMatch(in_port=in_port,
                                        eth_src=src,
                                        eth_dst=dst,
                                        eth_type=eth_type)
                actions = [parser.OFPActionOutput(out_port)]
                self.add_flow(datapath, 100, 0, match, actions)
